查看原文
其他

死磕Java并发:J.U.C之Condition

chenssy 程序猿DD 2019-07-13

作者:chenssy 

来源:http://cmsblogs.com/?p=2222


在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):


Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。 

  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 

  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。 

  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。 

  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。 

  6. signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。 

  7. signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。 


Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。


1、Condtion的实现 


获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。


Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:


  • 等待队列 


每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的定义就明白了:


 从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:


Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。


Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。


  • 等待 


调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。


  1. public final void await() throws InterruptedException {



  2.    // 当前线程中断



  3.    if (Thread.interrupted())



  4.        throw new InterruptedException();



  5.    //当前线程加入等待队列



  6.    Node node = addConditionWaiter();



  7.    //释放锁



  8.    long savedState = fullyRelease(node);



  9.    int interruptMode = 0;



  10.    /**



  11.     * 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待



  12.     * 直到检测到此节点在同步队列上



  13.     */



  14.    while (!isOnSyncQueue(node)) {



  15.        //线程挂起



  16.        LockSupport.park(this);



  17.        //如果已经中断了,则退出



  18.        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)



  19.            break;



  20.    }



  21.    //竞争同步状态



  22.    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)



  23.        interruptMode = REINTERRUPT;



  24.    //清理下条件队列中的不是在等待条件的节点



  25.    if (node.nextWaiter != null) // clean up if cancelled



  26.        unlinkCancelledWaiters();



  27.    if (interruptMode != 0)



  28.        reportInterruptAfterWait(interruptMode);



  29. }



此段代码的逻辑是:首先将当前线程新建一个节点同时加入到条件队列中,然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。


加入条件队列(addConditionWaiter())源码如下:


  1. private Node addConditionWaiter() {



  2.    Node t = lastWaiter;    //尾节点



  3.    //Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点



  4.    if (t != null && t.waitStatus != Node.CONDITION) {



  5.        //清除条件队列中所有状态不为Condition的节点



  6.        unlinkCancelledWaiters();



  7.        t = lastWaiter;



  8.    }



  9.    //当前线程新建节点,状态CONDITION



  10.    Node node = new Node(Thread.currentThread(), Node.CONDITION);



  11.    /**



  12.     * 将该节点加入到条件队列中最后一个位置



  13.     */



  14.    if (t == null)



  15.        firstWaiter = node;



  16.    else



  17.        t.nextWaiter = node;



  18.    lastWaiter = node;



  19.    return node;



  20. }



该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清楚所有状态不为Condition的节点。


fullyRelease(Node node),负责释放该线程持有的锁。


  1. final long fullyRelease(Node node) {



  2.    boolean failed = true;



  3.    try {



  4.        //节点状态--其实就是持有锁的数量



  5.        long savedState = getState();



  6.        //释放锁



  7.        if (release(savedState)) {



  8.            failed = false;



  9.            return savedState;



  10.        } else {



  11.            throw new IllegalMonitorStateException();



  12.        }



  13.    } finally {



  14.        if (failed)



  15.            node.waitStatus = Node.CANCELLED;



  16.    }



  17. }



isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true。


  1. final boolean isOnSyncQueue(Node node) {



  2.    //状态为Condition,获取前驱节点为null,返回false



  3.    if (node.waitStatus == Node.CONDITION || node.prev == null)



  4.        return false;



  5.    //后继节点不为null,肯定在CLH同步队列中



  6.    if (node.next != null)



  7.        return true;





  8.    return findNodeFromTail(node);



  9. }



unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除。


  1.    private void unlinkCancelledWaiters() {



  2.        Node t = firstWaiter;



  3.        Node trail = null;



  4.        while (t != null) {



  5.            Node next = t.nextWaiter;



  6.            if (t.waitStatus != Node.CONDITION) {



  7.                t.nextWaiter = null;



  8.                if (trail == null)



  9.                    firstWaiter = next;



  10.                else



  11.                    trail.nextWaiter = next;



  12.                if (next == null)



  13.                    lastWaiter = trail;



  14.            }



  15.            else



  16.                trail = t;



  17.            t = next;



  18.        }



  19.    }



  • 通知 


调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。


  1. public final void signal() {



  2.    //检测当前线程是否为拥有锁的独



  3.    if (!isHeldExclusively())



  4.        throw new IllegalMonitorStateException();



  5.    //头节点,唤醒条件队列中的第一个节点



  6.    Node first = firstWaiter;



  7.    if (first != null)



  8.        doSignal(first);    //唤醒



  9. }



该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。


doSignal(Node first):唤醒头节点。


  1. private void doSignal(Node first) {



  2.    do {



  3.        //修改头结点,完成旧头结点的移出工作



  4.        if ( (firstWaiter = first.nextWaiter) == null)



  5.            lastWaiter = null;



  6.        first.nextWaiter = null;



  7.    } while (!transferForSignal(first) &&



  8.            (first = firstWaiter) != null);



  9. }



doSignal(Node first)主要是做两件事:

  1. 修改头节点;

  2. 调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。


transferForSignal(Node first)源码如下:


  1. final boolean transferForSignal(Node node) {



  2.    //将该节点从状态CONDITION改变为初始状态0,



  3.    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))



  4.        return false;





  5.    //将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点



  6.    Node p = enq(node);



  7.    int ws = p.waitStatus;



  8.    //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒



  9.    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))



  10.        LockSupport.unpark(node.thread);



  11.    return true;



  12. }



整个通知的流程如下:

  1. 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。 

  2. 如果线程已经获取了锁,则将唤醒条件队列的首节点。 

  3. 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中 。

  4. 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。 


  • 总结 


一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。


当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。


2、Condition的应用 


只知道原理,如果不知道使用那就坑爹了,下面是用Condition实现的生产者消费者问题:

public class Condition{


   private LinkedList<String> buffer; //容器
   private int maxSize ;
   private Lock lock;
   private Condition fullCondition;
   private Condition notFullCondition;


  1. ConditionTest(int maxSize){



  2.    this.maxSize = maxSize;



  3.    buffer = new LinkedList<String>();



  4.    lock = new ReentrantLock();



  5.    fullCondition = lock.newCondition();



  6.    notFullCondition = lock.newCondition();



  7. }





  8. public void set(String string) throws InterruptedException {



  9.    lock.lock();    //获取锁



  10.    try {



  11.        while (maxSize == buffer.size()){



  12.            notFullCondition.await();       //满了,添加的线程进入等待状态



  13.        }





  14.        buffer.add(string);



  15.        fullCondition.signal();



  16.    } finally {



  17.        lock.unlock();      //记得释放锁



  18.    }



  19. }





  20. public String get() throws InterruptedException {



  21.    String string;



  22.    lock.lock();



  23.    try {



  24.        while (buffer.size() == 0){



  25.            fullCondition.await();



  26.        }



  27.        string = buffer.poll();



  28.        notFullCondition.signal();



  29.    } finally {



  30.        lock.unlock();



  31.    }



  32.    return string;



  33. }

  34. }



- END -

 往期推荐:

  • 死磕Java系列:

  1. 深入分析ThreadLocal

  2. 深入分析synchronized的实现原理

  3. 深入分析volatile的实现原理

  4. Java内存模型之happens-before

  5. Java内存模型之重排序

  6. Java内存模型之分析volatile

  7. Java内存模型之总结

  8. J.U.C之AQS简介

  9. J.U.C之AQS:CLH同步队列

  10. J.U.C之AQS同步状态的获取与释放

  11. J.U.C之AQS阻塞和唤醒线程

  12. J.U.C之重入锁:ReentrantLock

……


  • Spring系列:

  1. Spring Cloud Zuul中使用Swagger汇总API接口文档

  2. Spring Cloud Config Server迁移节点或容器化带来的问题

  3. Spring Cloud Config对特殊字符加密的处理

  4. Spring Boot使用@Async实现异步调用:使用Future以及定义超时

  5. Spring Cloud构建微服务架构:分布式配置中心(加密解密)

  6. Spring Boot快速开发利器:Spring Boot CLI

……

可关注我的公众号

深入交流、更多福利

扫码加入我的知识星球

点击“阅读原文”,看本号其他精彩内容

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存